//++
//
// Copyright (c) Microsoft Corporation.  All rights reserved.
//
// Module Name:
//
//  Schedule.cs
//    
// Abstract:
//
//  This module implements the Schedule class. This class encapsulates the reminders' schedule. It uses a time-sorted list
//  for storing the reminders and accessing them in order. Access to items in the list is through a binary search based on the 
//  reminder due time. The Schedule class also stores the reminders in a dictionary. This is to make finding the reminders 
//  easier when they are updated. The reminders are stored in the dictionary using the notification ID as the key.
//  
//--

using System;
using System.Collections.Generic;
using System.Diagnostics;

using Microsoft.OfficeCommunicationsServer.Applications.Common;

namespace Microsoft.EnhancedReminders.Scheduler
{
    /// <summary>
    /// Encapsulates the reminders' schedule.
    /// </summary>
    internal sealed class Schedule
    {
        // Time-sorted list of scheduled reminders
        private List<NotificationItem> _list;
        
        // Collection of scheduled reminders, stored using the notification ID as the key
        private Dictionary<string, NotificationItem[]> _dictionary;

        // Synchronizes access to the list and dictionary above
        private readonly object _syncRoot = new object();

        /// <summary>
        /// ctor.
        /// </summary>
        /// <param name="exchangeUsers">Source of the ItemUpdated and ItemDeleted events</param>
        internal Schedule(ExchangeUsers exchangeUsers)
        {
            _list = new List<NotificationItem>();
            _dictionary = new Dictionary<string, NotificationItem[]>();

            // exchangeUsers should only be null when this method is called from the unit tests
            if (exchangeUsers != null)
            {
                exchangeUsers.ItemUpdated += ExchangeUsers_ItemUpdated;
                exchangeUsers.ItemDeleted += ExchangeUsers_ItemDeleted;
            }
        }

        /// <summary>
        /// Raised when the next item in the schedule is updated.
        /// </summary>
        internal event EventHandler<EventArgs> Updated;

        /// <summary>
        /// Pops the next scheduled reminder from the schedule.
        /// </summary>
        /// <returns>
        /// The next scheduled reminder, or null if there are no scheduled reminders.
        /// </returns>
        internal NotificationItem RemoveNext()
        {
            NotificationItem notificationItem = null;

            lock (_syncRoot)
            {
                if (_list.Count > 0)
                {
                    notificationItem = _list[0];
                    // Remove the first notification from the list and the dictionary
                    _list.RemoveAt(0);
                    RemoveNotification(notificationItem);
                }
            }

            return notificationItem;
        }

        /// <summary>
        /// Gets the time of the next scheduled reminder, or DateTime.MaxValue if there are no scheduled reminders.
        /// </summary>
        internal DateTime NextTime
        {
            get
            {
                lock (_syncRoot)
                {
                    return _list.Count > 0 ? _list[0].NotificationTime : DateTime.MaxValue;
                }
            }
        }

        /// <summary>
        /// Handles updates to enhanced reminders.
        /// </summary>
        /// <param name="sender">Not used</param>
        /// <param name="args">Contains the reminder being updated</param>
        internal void ExchangeUsers_ItemUpdated(object sender, ItemUpdatedEventArgs args)
        {
            Debug.Assert(args != null, "args != null");
            Debug.Assert(args.NotificationItems != null, "notificationItems != null");
            Debug.Assert(args.NotificationItems.Length > 0, "notificationItems.Length > 0");
            Debug.Assert(!String.IsNullOrEmpty(args.Id), "!String.IsNullOrEmpty(args.Id)");

            NotificationItem[] existingNotificationItems = null;

            bool headChanged = false;

            lock (_syncRoot)
            {
                if (_dictionary.TryGetValue(args.Id, out existingNotificationItems))
                {
                    for (int i = 0; i < existingNotificationItems.Length; i++)
                    {
                        if (RemoveItemInList(existingNotificationItems[i]))
                        {
                            headChanged = true;
                        }
                    }
                }

                for (int i = 0; i < args.NotificationItems.Length; i++)
                {
                    Debug.Assert(args.NotificationItems[i].Id.Id == args.Id, "args.NotificationItems[i].Id.Id == args.Id");
                    
                    Logger.WriteTraceInformation(args.NotificationItems[i].Context, "Updating schedule with notification: {0}", args.NotificationItems[i]);
                    
                    if (InsertItemInList(args.NotificationItems[i]))
                    {
                        headChanged = true;
                    }
                }

                _dictionary[args.Id] = args.NotificationItems;
            }

            if (headChanged)
            {
                RaiseScheduleUpdated();
            }
        }

        /// <summary>
        /// Handles deletion of enhanced reminders.
        /// </summary>
        /// <param name="sender">Not used</param>
        /// <param name="args">Identifies the reminder being deleted</param>
        internal void ExchangeUsers_ItemDeleted(object sender, ItemDeletedEventArgs args)
        {
            Debug.Assert(args != null, "args != null");
            Debug.Assert(!String.IsNullOrEmpty(args.Id), "!String.IsNullOrEmpty(args.Id)");
            
            NotificationItem[] existingNotificationItems = null;

            bool headChanged = false;

            lock (_syncRoot)
            {
                if (_dictionary.TryGetValue(args.Id, out existingNotificationItems))
                {
                    for (int i = 0; i < existingNotificationItems.Length; i++)
                    {
                        Debug.Assert(existingNotificationItems[i].Id.Id == args.Id, "existingNotificationItems[i].Id.Id == args.Id");
                        if (RemoveItemInList(existingNotificationItems[i]))
                        {
                            headChanged = true;
                        }
                    }

                    _dictionary.Remove(args.Id);
                }
            }

            if (existingNotificationItems != null)
            {
                for (int i = 0; i < existingNotificationItems.Length; i++)
                {
                    Logger.WriteTraceInformation(existingNotificationItems[i].Context, "Deleted notification from schedule: {0}", existingNotificationItems[i]);
                }
            }

            if (headChanged)
            {
                RaiseScheduleUpdated();
            }
        }

        /// <summary>
        /// Raises the ScheduleUpdated event.
        /// </summary>
        private void RaiseScheduleUpdated()
        {
            EventHandler<EventArgs> updated = Updated;

            if (updated != null)
            {
                updated(this, EventArgs.Empty);
            }
        }

        /// <summary>
        /// Removes the specified notification from the look-up table.
        /// </summary>
        /// <param name="notificationItem">Notification to be removed</param>
        /// <remarks>
        /// This method is called to remove an indivdual notification as opposed to all the notifications associated with a
        /// mail item. This method is called when a notification is delivered.
        /// </remarks>
        private void RemoveNotification(NotificationItem notificationItem)
        {
            NotificationItem[] existingNotificationItems = null;

            lock (_syncRoot)
            {
                // Find the entry for this notification in the look-up table
                if (_dictionary.TryGetValue(notificationItem.Id.Id, out existingNotificationItems))
                {
                    Debug.Assert(existingNotificationItems.Length > 0, "existingNotificationItems.Length > 0");

                    if (existingNotificationItems.Length > 1)
                    {
                        int j = 0;

                        NotificationItem[] newNotificationItems = new NotificationItem[existingNotificationItems.Length - 1];

                        // Copy all the notifications apart from the one to be removed
                        for (int i = 0; i < existingNotificationItems.Length; i++)
                        {
                            if (!existingNotificationItems[i].Equals(notificationItem))
                            {
                                Debug.Assert(j < newNotificationItems.Length, "j < newNotificationItems.Length");
                                newNotificationItems[j++] = existingNotificationItems[i];
                            }
                        }

                        Debug.Assert(j == existingNotificationItems.Length - 1, "j == existingNotificationItems.Length - 1");

                        _dictionary[notificationItem.Id.Id] = newNotificationItems;
                    }
                    else
                    {
                        // This is the only notification in this entry so remove this entry form the look-up table
                        Debug.Assert(existingNotificationItems[0].Equals(notificationItem), "existingNotificationItems[0].Equals(notificationItem)");
                        _dictionary.Remove(notificationItem.Id.Id);
                    }
                }
                else
                {
                    // The specified notification should always be in the look-up table
                    Debug.Fail("_dictionary.TryGetValue(notificationItem.Id.Id, out existingNotificationItems)");
                }
            }
        }

        /// <summary>
        /// Inserts the specified reminder into the correct position in the time-sorted list of scheduled reminders.
        /// </summary>
        /// <param name="notificationItem">The reminder to be inserted</param>
        /// <returns>
        /// true if the first entry in the list is modified; false otherwise.
        /// </returns>
        private bool InsertItemInList(NotificationItem notificationItem)
        {
            int index = _list.BinarySearch(notificationItem);

            if (index < 0)
            {
                index = ~index;
            }

            _list.Insert(index, notificationItem);

            return index == 0;
        }

        /// <summary>
        /// Removes the specified reminder from the schedule
        /// </summary>
        /// <param name="notificationItem">Reminder to be removed</param>
        /// <returns>
        /// true if the first entry in the list is modified; false otherwise.
        /// </returns>
        private bool RemoveItemInList(NotificationItem notificationItem)
        {
            int index = _list.BinarySearch(notificationItem);
            
            Debug.Assert(index >= 0, "index should not be negative");

            if (index >= 0)
            {
                if (!_list[index].Equals(notificationItem))
                {
                    // The binary search will just return a notification at the correct time. It will not necessarily be the 
                    // actual notification, so have to do a linear search to find the actual notification. The search starts
                    // from the index found by the binary search for the sake of efficiency.
                    index = FindIndexOfItem(notificationItem, index);

                    Debug.Assert(index >= 0, "index should still not be negative");
                }

                _list.RemoveAt(index);
            }

            return index == 0;
        }

        /// <summary>
        /// Does a linear search for the specified reminder from the specified starting point.
        /// </summary>
        /// <param name="notificationItem">Reminder being searched for</param>
        /// <param name="index">Starting point of the search</param>
        /// <remarks>
        /// The search searchs outwards from the specified starting point.
        /// </remarks>
        /// <returns>
        /// The index of the specified reminder in the time-sorted list, or -1 if the reminder can't be found.
        /// </returns>
        private int FindIndexOfItem(NotificationItem notificationItem, int index)
        {
            int indexUp = index + 1;
            int indexDown = index - 1;

            while (indexUp < _list.Count || indexDown >= 0)
            {
                if (indexUp < _list.Count)
                {
                    // Search up
                    if (_list[indexUp].Equals(notificationItem))
                    {
                        return indexUp;
                    }
                    else
                    {
                        indexUp++;
                    }
                }

                if (indexDown >= 0)
                {
                    // Search down
                    if (_list[indexDown].Equals(notificationItem))
                    {
                        return indexDown;
                    }
                    else
                    {
                        indexDown--;
                    }
                }
            }

            return -1;
        }
    }
}
